package owg.util.editor2d.modes;

import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.lang.annotation.Annotation;

import com.jogamp.opengl.GL;
import javax.swing.JOptionPane;

import owg.util.Calc;
import owg.util.data.PField;
import owg.util.data.PFieldUtil;
import owg.util.editor2d.Editor2D;
import owg.util.editor2d.entity.Input;
import owg.util.editor2d.entity.Duplex;
import owg.util.editor2d.entity.Output;
import owg.util.editor2d.entity.Placeable2D;
import owg.util.editor2d.operations.LinkOperation;
import owg.util.editor2d.operations.UnlinkOperation;
import owg.util.euclidian.Compass;
import owg.util.euclidian.V2D;
import owg.util.opengl.BlendMode;
import owg.util.opengl.ColorF;
import owg.util.opengl.EGL;
import owg.util.opengl.ImmediateModeWrapper;
import owg.util.opengl.TTFFontRenderer;

public class Link extends Mode
{
	private final static String H_STR = "Duplex";
	private final static String F_STR = "Input";
	private final static String M_STR = "Output";
	private final static String C_NAME = "Connector";
	private final static String LINK_EXPLANATION = 
		M_STR+" "+C_NAME+"s can only be connected to "+F_STR+" "+C_NAME+"s on a different object, forming directional connections.\n"+
		H_STR+" "+C_NAME+"s can only be connected to "+H_STR+" "+C_NAME+"s on a different object, forming nondirectional connections.\n"+
		"Also, a "+C_NAME+" can only accept a connection if the other object is a subtype of the "+C_NAME+"'s type. \n"+
		"For example, a "+F_STR+" "+C_NAME+" with type Animal can only be connected to a "+M_STR+" "+C_NAME+" on an object which is an instance of Animal,\n"+
		"like a Cat or a Dog. If the object holding the "+M_STR+" "+C_NAME+" is a Boat(i.e. not an Animal), then the connection is illegal.\n"+
		"This also applies in the other direction: The object which holds the "+F_STR+" "+C_NAME+" must be an instance of the type of the "+M_STR+" "+C_NAME+".";
	private final static int EDGE = 72;
	
	public final static byte MASK_INPUT = 1;
	public final static byte MASK_OUTPUT = 2;
	public final static byte MASK_INPUT_AND_OUTPUT = 3;
	
	private final static byte INVALID_SRC_OBJECT = -1;
	private final static byte INVALID_DST_OBJECT = -2;
	private final static byte SELECT_SRC_FIELD = 0;
	private final static byte SELECT_DST_OBJECT = 1;
	private final static byte SELECT_DST_FIELD = 2;
	byte state;
	boolean isLMBPressing = false;
	int pressedKey = -1;
	Placeable2D srcObject;
	Choice[] srcOptions;
	Choice srcChoice;
	Placeable2D dstObject;
	Choice[] dstOptions;
	Choice dstChoice;
	
	private class Choice 
	{
		final Placeable2D obj;
		final PField<?> field;
		final String name;
		final byte func;
		final double radianThreshold;
		final double angle;
		final char mnemonic;
		final String error;
		public Choice(Placeable2D obj, PField<?> field, String name, char mnemonic, byte func, double radianThreshold, double angle, String error)
		{
			super();
			this.obj = obj;
			this.name = name;
			this.mnemonic = mnemonic;
			this.field = field;
			this.func = func;
			this.radianThreshold = radianThreshold;
			this.angle = angle;
			this.error = error;
		}
	}
	
	public Link(Editor2D e)
	{
		super(e);
		srcObject = e.getPivotObject();
		if(srcObject == null)
			srcOptions = null;
		else
			srcOptions = makeChoices(null, srcObject);
		srcChoice = null;
		if(srcOptions == null || srcOptions.length == 0)
			state = INVALID_SRC_OBJECT;
		else
			state = SELECT_SRC_FIELD;
		dstObject = null;
		dstOptions = null;
		dstChoice = null;
		if(srcObject == null)
			editor.editorUI().message("Link...");
		else
			editor.editorUI().message("Link "+srcObject.getName()+"...");
		updateChoice(null);
	}
	private boolean updateChoice(Character mnemonic)
	{
		if(state == SELECT_DST_OBJECT)
		{
			Placeable2D h = editor.getHighlightedObject();
			editor.editorUI().replaceMessage("Link "+srcObject.getName()+"'s "+srcChoice.name+" to"+(h==null?"":(" "+h.getName()))+"...");
		}
		else
		{
			Placeable2D obj;
			Choice[] options;
			if(state == SELECT_SRC_FIELD)
			{
				obj = srcObject;
				options = srcOptions;
			}
			else if(state == SELECT_DST_FIELD)
			{
				obj = dstObject;
				options = dstOptions;
			}
			else
			{
				obj = null;
				options = null;
			}
			
			if(options != null)
			{
				Choice ch = null;
				if(mnemonic == null)
				{
					V2D clampedPos = clampedPos(obj, new byte[1]);
					V2D mouse = editor.eventHandler().getMousePosition();
					double radian = Calc.dir(clampedPos.x(), clampedPos.y(), mouse.x(), mouse.y());
					for(Choice o : options)
					{
						if(radian < o.radianThreshold)
						{
							ch = o;
							break;
						}
					}
				}
				else
				{
					for(Choice o : options)
					{
						if(o.mnemonic == mnemonic.charValue())
						{
							ch = o;
							break;
						}
					}
				}

				if(state == SELECT_SRC_FIELD)
				{
					if(ch != null)
						srcChoice = ch;
					editor.editorUI().replaceMessage("Link "+srcObject.getName()+(ch==null?"...":"'s "+ch.name+" to..."));	
				}
				else
				{
					editor.editorUI().replaceMessage("Link "+srcObject.getName()+"'s "+srcChoice.name+" to "+dstObject.getName()+(ch==null?"...":"'s "+ch.name));
					dstChoice = ch;
				}
				return ch != null;
			}
		}
		return false;
	}
	private Choice[] makeChoices(Choice counterpart, Placeable2D obj)
	{
		PField<?>[] fields = PFieldUtil.getPFields(obj);
		Choice[] choices = new Choice[fields.length];
		double increment = Math.PI*2.0/fields.length;
		double threshold = 0;
		int mIndex = 0;
		
		for(int i = 0; i<fields.length; i++)
		{
			byte func = getFunc(fields[i], obj);
			
			String name = PFieldUtil.nameOf(fields[i], obj);
			double angle = threshold+increment/2;
			threshold += increment;
			
			final String error;
			if(func == 0)
				error = "Must be "+M_STR+", "+F_STR+" or "+H_STR+"...";
			else if(counterpart != null)
				error = fits(counterpart.obj.getClass(), counterpart.field.getType(), counterpart.func, obj.getClass(), fields[i].getType(), func);
			else
				error = null;
			
			final char mnemonic;
			if(error != null)
				mnemonic = '\0';
			else if (mIndex < 10)
				mnemonic = (char)((int)'1'+mIndex);
			else if(mIndex == 10)
				mnemonic = '0';
			else if(mIndex < 10+26)
				mnemonic = (char)((int)'a'+(mIndex-10));
			else if(mIndex < 10+26+26)
				mnemonic = (char)((int)'A'+(mIndex-10-26));
			else
				mnemonic = '\0';
			if(mnemonic != '\0')
				mIndex++;
			
			choices[i] = new Choice(obj, fields[i], name, mnemonic, func, threshold, angle, error);
		}
		return choices;
	}
	
	public static byte getFunc(PField<?> pField, Placeable2D obj)
	{
		Annotation[] as = PFieldUtil.getAnnotations(pField, obj);
		byte func = 0;
		for(Annotation a : as)
		{
			if(a instanceof Output)
				func |= MASK_OUTPUT;
			else if(a instanceof Input)
				func |= MASK_INPUT;
			else if(a instanceof Duplex)
				func |= MASK_INPUT_AND_OUTPUT;
			
			if(func == MASK_INPUT_AND_OUTPUT)
				break;
		}
		return func;
	}
	public static String fits(Class<?> objA, Class<?> connectorA, byte funcA, Class<?> objB, Class<?> connectorB, byte funcB)
	{
		//If one is herm then both must be herm
		if((funcA == MASK_INPUT_AND_OUTPUT) != (funcB == MASK_INPUT_AND_OUTPUT))
			return noFitString(funcA, funcB);
		//If A is input then B must be output
		if((funcA & MASK_INPUT) != 0)
		{
			if((funcB & MASK_OUTPUT) == 0)
				return noFitString(funcA, funcB);
			//If there is a connection from A to B then B must be assignable to A
			if (!connectorA.isAssignableFrom(objB))
				return incompatibleString(objB, connectorA);
		}
		//If A is output then B must be input
		if((funcA & MASK_OUTPUT) != 0)
		{
			if((funcB & MASK_INPUT) == 0)
				return noFitString(funcA, funcB);
			//If there is a connection from A to B then B must be assignable to A
			if (!connectorB.isAssignableFrom(objA))
				return incompatibleString(objA, connectorB);
		}
		//There is no need to do further tests because:
		//-We know neither is asexual
		//-We know either both or none are herm
		// --If both are herm, then both tests for A will pass and B will be fully tested
		// --Otherwise, B is either the same sex as A, or the opposite sex of A
		//  .: In this case, a single test for A will always reveal whether B is a go or no-go.
		return null;
	}
	private static String incompatibleString(Class<?> obj, Class<?> connector)
	{
		return "Illegal: "+obj.getSimpleName()+" </: "+connector.getSimpleName();
	}
	private static String noFitString(byte funcA, byte funcB)
	{
		return "Mismatch: "+funcString(funcA)+" <-/-> "+funcString(funcB);
	}
	@Override
	public void render(EGL<?> egl)
	{
		if(state == SELECT_DST_FIELD)
			display(egl, dstOptions, dstChoice);
		else if(state == SELECT_SRC_FIELD)
			display(egl, srcOptions, srcChoice);
		else if(state == SELECT_DST_OBJECT)
		{
			float[] c = Editor2D.bColors[3].getFloat();
			V2D mouse = editor.eventHandler().getMousePosition();
			if(editor.settings.snapSelect.get())
				editor.eventHandler().correctToGrid(mouse);
			editor.putLink(srcObject.getX(), srcObject.getY(), mouse.x(), mouse.y(), srcChoice.func, c);
		}
		else 
		{
			egl.setColor(new ColorF(0f, 0f, 0f, 0.5f));
			egl.nwSquare.render(editor.camera.pos, editor.camera.size.x(), editor.camera.size.y(), 1);
			final Placeable2D obj;
			final String message;
			if(state == INVALID_SRC_OBJECT)
			{
				obj = srcObject;
				if(obj == null)
					message = "No object selected...";
				else
					message = "The pivot object has no non-hidden "+C_NAME+"s!";
			}
			else
			{
				obj = dstObject;
				message = "The other object has no non-hidden "+C_NAME+"s!";
			}
			float cx = (float)(editor.camera.pos.x()+editor.camera.size.x()/2), 
				cy = (float)(editor.camera.pos.y()+editor.camera.size.y()/2);
			
			TTFFontRenderer font = editor.getFont();
			font.begin(egl);
			font.draw(obj==null?"NULL":obj.getName(), cx,cy-font.getSize(), Compass.CENTER);
			font.draw(message, cx,cy+font.getSize(), Compass.CENTER);
			font.end();
			font.setColor(ColorF.WHITE);
		}
		
	}
	
	private void display(EGL<?> egl, Choice[] display, Choice currentChoice)
	{
		byte[] isClamped = new byte[1];
		ImmediateModeWrapper<?> im = egl.immediateMode();
		V2D p0 = editor.camera.pos;
		V2D p1 = editor.camera.pos.clone().add(editor.camera.size);
		V2D c = clampedPos(currentChoice.obj, isClamped);
		
		TTFFontRenderer font = editor.getFont();
		for(int i = 0; i<display.length; i++)
		{
			int cIndex;
			Choice o = display[i];
			if(o.error != null)
			{
				cIndex = 0;
				egl.setColor(new ColorF(0.33f, 0.33f, 0.33f, 0.75f));
			}
			else if(currentChoice == o)
			{	
				if(isLMBPressing || pressedKey != -1)
					cIndex = 3;
				else
					cIndex = 1;
				egl.setColor(new ColorF(0f, 0f, 0f, 0.5f));
			}
			else
			{
				cIndex = 0;
				egl.setColor(new ColorF(0f, 0f, 0f, 0.75f));
			}
			
			double w, h;
			if(o.angle > Math.PI/2.0 && o.angle < Math.PI*3.0/2.0)
				w = c.x()-p0.x();
			else
				w = p1.x()-c.x();
			if(o.angle < Math.PI)
				h = c.y()-p0.y();
			else
				h = p1.y()-c.y();
			V2D n = new V2D(Math.cos(o.angle), -Math.sin(o.angle));
			double fracX = n.dot(1, 0);
			double fracY = n.dot(0, 1);
			final double hr, vr;
			if(fracX == 0)
				hr = Double.MAX_VALUE;
			else
				hr = w/Math.abs(fracX);
			if(fracY == 0)
				vr = Double.MAX_VALUE;
			else
				vr = h/Math.abs(fracY);
			
			double r = Math.min(hr, vr)*0.667;
			
			double diag = editor.camera.size.manLen();
			Choice po = display[Calc.cyclic(i-1, display.length)];
			im.glBegin(false, GL.GL_TRIANGLES, 2, false, 0, 0);
			im.glVertex2d(c.x(), c.y());
			im.glVertex2d(c.x()+Math.cos(o.radianThreshold)*(diag), c.y()-Math.sin(o.radianThreshold)*(diag));
			im.glVertex2d(c.x()+Math.cos(po.radianThreshold)*(diag), c.y()-Math.sin(po.radianThreshold)*(diag));
			im.glEnd();
			//m.push();
			//m.translatef((float) (editor.camera.pos.x()+o.viewPos.x())-spacing, (float) (editor.camera.pos.y()+o.viewPos.y())-spacing, 0);
			//m.scalef(o.stringWidth+spacing*2, o.stringHeight+spacing*2, 1);
			//egl.nwSquare.render();
			//m.pop();
			float textX = (float)(c.x()+n.x()*r);
			float textY = (float)(c.y()+n.y()*r);
			if(o.error != null)
				font.setColor(ColorF.DKGRAY);
			else
				font.setColor(Editor2D.bColors[cIndex]);
			font.begin(egl);
			font.draw(o.name, textX, textY-font.getSize()*0.5f, Compass.CENTER);
			font.draw(funcString(o.func), textX, textY+font.getSize()*0.5f, Compass.CENTER);
			font.draw(o.field.getType().getSimpleName(), textX, textY+font.getSize()*1.5f, Compass.CENTER);
			if(o.mnemonic != '\0')
				font.draw("["+o.mnemonic+']', textX, textY-font.getSize()*1.5f, Compass.CENTER);
			font.end();
		}

		if(currentChoice.error != null)
		{
			font.setColor(ColorF.WHITE);
			font.render(egl, currentChoice.error, 
				(int)(editor.camera.pos.x()+editor.camera.size.x()/2), 
				(int)(editor.camera.pos.y()+editor.camera.size.y()), Compass.SOUTH);
		}
		if(isClamped[0] > 0)
		{
			egl.setStippleEnabled(true);
			egl.setColor(ColorF.WHITE);
			egl.setBlendMode(BlendMode.INVERT);
			im.glBegin(false, GL.GL_LINES, 2, false, 0, 0);
			im.glVertex2d(c.x(), c.y());
			im.glVertex2d(currentChoice.obj.getX(), currentChoice.obj.getY());
			im.glEnd();
			egl.setBlendMode(BlendMode.NORMAL);
			egl.setStippleEnabled(false);
		}
	}
	private static String funcString(byte func)
	{
		if(func == MASK_INPUT_AND_OUTPUT)
			return H_STR;
		else if(func == MASK_INPUT)
			return F_STR;
		else if(func == MASK_OUTPUT)
			return M_STR;
		else
			return "N/A";
	}
	private boolean next()
	{
		if(state == SELECT_SRC_FIELD)
		{
			if(srcChoice == null)
			{
				updateChoice(null);
				return false;
			}
			else
			{
				state++;
				updateChoice(null);
				return true;
			}
		}
		else if(state == SELECT_DST_OBJECT)
		{
			Placeable2D highlight = editor.getHighlightedObject();
			if(highlight == null)
			{
				terminate(true);
				return true;
			}
			else
			{
				dstObject = highlight;
				dstOptions = makeChoices(srcChoice, dstObject);
				dstChoice = null;
				if(dstOptions.length == 0)
					state = INVALID_DST_OBJECT;
				else
					state = SELECT_DST_FIELD;
				updateChoice(null);
				return true;
			}
		}
		else if(state == SELECT_DST_FIELD)
		{
			terminate(dstChoice == null);
			return true;
		}
		return false;
	}
	
	private V2D clampedPos(Placeable2D obj, byte[] clamps)
	{
		V2D c = new V2D(obj.getX(), obj.getY());
		c.p[0] = Calc.limit(c.p[0], editor.camera.pos.x()+EDGE, editor.camera.pos.x()+editor.camera.size.x()-EDGE, clamps, (byte) 0);
		c.p[1] = Calc.limit(c.p[1], editor.camera.pos.y()+EDGE, editor.camera.pos.y()+editor.camera.size.y()-EDGE, clamps, (byte) 0);
		return c;
	}
	@Override
	public void mouse(MouseEvent e)
	{
		if(e.getID() == MouseEvent.MOUSE_PRESSED && (e.getButton() == MouseEvent.BUTTON3 ||
			(e.getButton() == MouseEvent.BUTTON1 && state < 0)))
			terminate(true);
		else if(pressedKey == -1)
		{
			if(e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON1)
				isLMBPressing = true;
			else if(e.getID() == MouseEvent.MOUSE_RELEASED && e.getButton() == MouseEvent.BUTTON1 && isLMBPressing)
			{
				next();
				isLMBPressing = false;
			}
			else if(e.getID() == MouseEvent.MOUSE_MOVED || e.getID() == MouseEvent.MOUSE_DRAGGED)
			{
				updateChoice(null);
			}
		}
	}
	
	@Override
	public void key(KeyEvent e)
	{
		if(!isLMBPressing)
		{
			if(e.getID() == KeyEvent.KEY_PRESSED && pressedKey == -1)
			{
				boolean b = updateChoice(e.getKeyChar());
				if(b)
					pressedKey = e.getKeyCode();
			}
			if(e.getID() == KeyEvent.KEY_RELEASED && e.getKeyCode() == pressedKey)
			{
				pressedKey = -1;
				next();
			}
		}
		if(e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ESCAPE)
			terminate(true);
	}
	
	@Override
	public void terminate(boolean cancel)
	{
		if(cancel)
		{
			editor.editorUI().replaceMessage("Linking Cancelled.");
		}
		else
		{
			try
			{
				if(srcObject == null)
					throw new IllegalArgumentException("Pivot object cannot be null...");
				if(srcChoice == null)
					throw new IllegalArgumentException("You must choose a "+C_NAME+" on the pivot object...");
				if(dstObject == null)
					throw new IllegalArgumentException("You must choose a destination object...");
				if(dstChoice == null)
					throw new IllegalArgumentException("Must choose a "+C_NAME+" on the destination object...");
				if (dstChoice.error != null)
					throw new IllegalArgumentException(dstChoice.error+"\n\n"+LINK_EXPLANATION);
				
				LinkOperation op = new LinkOperation(editor, editor.indexOf(srcObject), srcChoice.name, editor.indexOf(dstObject), dstChoice.name);
				op.applyWithoutCatch(editor);
				UnlinkOperation inv = new UnlinkOperation(op);
				
				editor.execute(op, inv, false, "Linked "+srcObject.getName()+"."+srcChoice.name+" to "+
					dstObject.getName()+"."+dstChoice.name+".", true, false);
			}
			catch(IllegalArgumentException e)
			{
				JOptionPane.showMessageDialog(null, e.getMessage(), "Cannot link objects", JOptionPane.INFORMATION_MESSAGE);
			}
		}
		editor.modeFinished();
	}	
	@Override
	public boolean shouldUpdateHighlight()
	{
		return state == SELECT_DST_OBJECT;
	}
}
